feat: add 'mobilecli device logs' support for streaming logs from mobile devices#195
feat: add 'mobilecli device logs' support for streaming logs from mobile devices#195
Conversation
Adds real-time log streaming from iOS simulators (via xcrun simctl log stream) and real iOS devices (via go-ios syslog_relay). Android and remote devices return "not yet supported". Supports --limit flag to stop after N entries.
Filters are applied before --limit, so the user sees exactly N matching entries. --process does substring matching on the process name.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds a 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
commands/logs.go (1)
31-33: PID filter condition may have unintended behavior with PID 0.The condition
req.PID >= 0means PID 0 is treated as a valid filter value. If a user passes--pid 0, it would filter for processes with PID 0 (typically the kernel scheduler). This might be intentional, but consider whether-1should be the only "unset" sentinel, or if you want to use a pointer type (*int) to distinguish "not set" from "filter for PID 0".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@commands/logs.go` around lines 31 - 33, The PID-check treats 0 as a valid filter because it uses req.PID >= 0; change the filter to distinguish "unset" from a real PID by making req.PID a pointer (*int) or by using -1 as the explicit unset sentinel; then update the check in the logs filtering code to something like "if req.PID != nil && entry.PID != *req.PID { return true }" (or if using sentinel: "if req.PID != -1 && entry.PID != req.PID { return true }") and adjust flag/constructors that populate req.PID accordingly so callers can actually signal "no PID filter".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@commands/logs.go`:
- Around line 35-37: The current handler silently swallows JSON encoding
failures by returning false on encoder.Encode(entry) and later returning
NewSuccessResponse("done"); modify the error path so encode failures are
propagated instead of masked: when encoder.Encode(entry) returns an error,
return a failure value (e.g., propagate the error to the caller or return a
NewErrorResponse with err.Error()) rather than returning false, and ensure the
function no longer returns NewSuccessResponse("done") when an encode error
occurred. Update the code around encoder.Encode(entry) to surface the actual err
and stop further processing, and adjust any callers of this function to handle
the propagated error if needed.
In `@devices/ios.go`:
- Around line 1667-1717: StreamLogs currently calls d.getEnhancedDevice()
directly which can fail on iOS 17+ if the tunnel isn't started; mirror other
methods (Reboot, LaunchApp, TerminateApp) by calling d.startTunnel() first and
returning any error. Modify StreamLogs to invoke d.startTunnel(), check and
return its error, then proceed to call d.getEnhancedDevice() and the rest of the
existing logic; ensure you reference the IOSDevice methods startTunnel and
getEnhancedDevice and preserve existing defer conn.Close() behavior.
---
Nitpick comments:
In `@commands/logs.go`:
- Around line 31-33: The PID-check treats 0 as a valid filter because it uses
req.PID >= 0; change the filter to distinguish "unset" from a real PID by making
req.PID a pointer (*int) or by using -1 as the explicit unset sentinel; then
update the check in the logs filtering code to something like "if req.PID != nil
&& entry.PID != *req.PID { return true }" (or if using sentinel: "if req.PID !=
-1 && entry.PID != req.PID { return true }") and adjust flag/constructors that
populate req.PID accordingly so callers can actually signal "no PID filter".
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 4c7acb32-e5d2-4071-b9d8-7d12c6621eaf
📒 Files selected for processing (7)
cli/logs.gocommands/logs.godevices/android.godevices/common.godevices/ios.godevices/remote.godevices/simulator.go
Streams logs via adb logcat with threadtime,year format. Parses using existing logcatLineRegex. Resolves PID to package name via one-time adb shell ps call for --process filtering. Adds tag field to LogEntry (populated for Android, omitted for iOS).
There was a problem hiding this comment.
♻️ Duplicate comments (1)
commands/logs.go (1)
34-36:⚠️ Potential issue | 🟠 MajorPropagate JSON encode failures instead of returning success.
At Line 34, encode errors stop the callback, but the error is discarded; then Line 47 can still return success. This masks partial stream failures.
Suggested fix
encoder := json.NewEncoder(os.Stdout) count := 0 + var encodeErr error err = device.StreamLogs(func(entry devices.LogEntry) bool { if req.Process != "" && entry.Process != req.Process { return true } if req.PID >= 0 && entry.PID != req.PID { return true } if err := encoder.Encode(entry); err != nil { + encodeErr = err return false } count++ if req.Limit > 0 && count >= req.Limit { return false } return true }) if err != nil { return NewErrorResponse(fmt.Errorf("error streaming logs: %w", err)) } + if encodeErr != nil { + return NewErrorResponse(fmt.Errorf("error encoding log entry: %w", encodeErr)) + } return NewSuccessResponse("done")Also applies to: 43-47
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@commands/logs.go` around lines 34 - 36, The JSON encode failures are being swallowed where encoder.Encode(entry) is called; change the callback to propagate the error instead of returning a bool success sentinel: when encoder.Encode(entry) (and the similar block at 43-47) returns an error, return that error (or set a named err variable and break) so the caller can detect and surface the failure, and update the outer caller/return path to return the error rather than always returning success; reference the encoder.Encode(entry) calls and the surrounding callback/handler to locate and update the code paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@commands/logs.go`:
- Around line 34-36: The JSON encode failures are being swallowed where
encoder.Encode(entry) is called; change the callback to propagate the error
instead of returning a bool success sentinel: when encoder.Encode(entry) (and
the similar block at 43-47) returns an error, return that error (or set a named
err variable and break) so the caller can detect and surface the failure, and
update the outer caller/return path to return the error rather than always
returning success; reference the encoder.Encode(entry) calls and the surrounding
callback/handler to locate and update the code paths.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: bcf44939-e9f5-4879-b35b-2ccb292cf83b
📒 Files selected for processing (3)
commands/logs.godevices/android.godevices/common.go
🚧 Files skipped from review as they are similar to previous changes (2)
- devices/common.go
- devices/android.go
- Fix --process flag description to say "exact match" - Split callback into matchesFilter and emit helpers - Extract processNameFromPath and atoiOrZero to common.go
Supports key=value (include) and key!=value (exclude) syntax. Multiple filters are ANDed. Valid keys: pid, process, tag, level, subsystem, category, message. Invalid keys return a clear error.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@devices/simulator.go`:
- Around line 1072-1116: The code currently treats any json.Decoder errors as
normal end-of-stream and kills the child without reaping it; update the
log-stream handling so that failures from decoder.Token() or decoder.Decode()
that are not io.EOF are returned as errors and the child is reaped: after
calling cmd.Process.Kill() invoke cmd.Wait() and include its error in the
returned fmt.Errorf; when decoder.Decode() returns io.EOF treat it as normal
end-of-stream, but for other errors return them (again ensuring cmd.Wait() is
called and its error combined); finally, don't ignore the result of cmd.Wait()
at the end—return any non-nil wait error. Add the io import and reference
functions/values like cmd.Start, decoder.Token, decoder.Decode,
cmd.Process.Kill, cmd.Wait, simctlLogEntry, and onLog when making these fixes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 4d003d51-ab06-4dfe-8267-12fe09f41d2e
📒 Files selected for processing (6)
README.mdcli/logs.gocommands/logs.godevices/common.godevices/ios.godevices/simulator.go
✅ Files skipped from review due to trivial changes (1)
- README.md
🚧 Files skipped from review as they are similar to previous changes (1)
- cli/logs.go
Replaces legacy syslog_relay (ASL) with structured os_trace_relay via go-ios v1.0.211's new ostrace package. Real iOS devices now expose the same subsystem/category/structured-level fields the simulator already provided, so cross-platform --filter keys work uniformly on hardware. Removes EventType from LogEntry: it was simctl-only metadata that could not be populated on iOS-real (the ostrace binding does not surface record types and we filter to log messages only) or Android (logcat has no equivalent), and no consumer read it. Drops the now-unused atoiOrZero helper. Documents device logs --filter in the root help text.
# Conflicts: # cli/root.go
iOS real device: StreamLogs now starts the iOS 17+ tunnel before opening the os_trace_relay connection, matching Reboot/LaunchApp/TerminateApp. Without this, ostrace.New fails on devices that need a tunnel. Simulator: surface real read failures from the simctl JSON stream and reap the child process on every error path. Previously, decoder.Token and decoder.Decode errors were swallowed as "stream ended" and cmd.Wait was ignored, so a non-zero simctl exit (bad UDID, device shutdown mid-stream) returned success and left a zombie. EOF from Decode is still treated as a normal end-of-stream; caller-initiated stops are tracked separately so the kill we issue ourselves is not reported as an error.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@devices/android.go`:
- Around line 1408-1450: The PID→process map is created once by calling
getPidToProcessMap() before starting adb logcat, so processes started after the
stream begins never get resolved; update the code inside the scanner loop to
refresh or requery the PID map when needed (e.g., call getPidToProcessMap()
again on a cache miss or at a regular interval) before assigning entry.Process,
referencing pidMap, getPidToProcessMap(), the scanner loop that iterates
parseLogcatLine(line), and the LogEntry.Process assignment so new processes
launched after log streaming starts will be correctly resolved for --process
filtering.
- Around line 1424-1459: StreamLogs currently swallows scanner and subprocess
errors; change it to propagate them: after the scanner loop check scanner.Err()
and return it if non-nil (unless we intentionally stopped because onLog returned
false), and after breaking/killing the child call cmd.Wait() and return its
error instead of always returning nil; use the onLog return value to distinguish
an intentional stop (treat as nil) from actual errors, and ensure any error from
cmd.Wait() is returned when the stop was not intentional. Reference: StreamLogs
function, scanner variable, onLog callback, and cmd.Wait()/cmd.Process.Kill()
calls.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d8bdd6cd-3f76-49de-9adb-4b9465c7d845
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (8)
README.mdcli/root.godevices/android.godevices/common.godevices/ios.godevices/remote.godevices/simulator.gogo.mod
✅ Files skipped from review due to trivial changes (2)
- cli/root.go
- README.md
🚧 Files skipped from review as they are similar to previous changes (1)
- devices/remote.go
| scanner := bufio.NewScanner(stdout) | ||
| for scanner.Scan() { | ||
| line := scanner.Text() | ||
|
|
||
| parsed := parseLogcatLine(line) | ||
| if parsed == nil { | ||
| continue | ||
| } | ||
|
|
||
| pid, _ := strconv.Atoi(parsed.PID) | ||
| level := logcatLevelMap[parsed.Level] | ||
| if level == "" { | ||
| level = parsed.Level | ||
| } | ||
|
|
||
| entry := LogEntry{ | ||
| Timestamp: parsed.Date + " " + parsed.Time, | ||
| PID: pid, | ||
| Level: level, | ||
| Tag: parsed.Tag, | ||
| Message: parsed.Message, | ||
| } | ||
|
|
||
| // resolve process name from ps map (for --process filtering) | ||
| if pidMap != nil { | ||
| entry.Process = pidMap[pid] | ||
| } | ||
|
|
||
| if !onLog(entry) { | ||
| _ = cmd.Process.Kill() | ||
| break | ||
| } | ||
| } | ||
|
|
||
| _ = cmd.Wait() | ||
| return nil |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's find the file and examine the target lines
git ls-files devices/android.goRepository: mobile-next/mobilecli
Length of output: 84
🏁 Script executed:
# Get the file size to ensure we can read it
wc -l devices/android.goRepository: mobile-next/mobilecli
Length of output: 89
🏁 Script executed:
# Read the target lines and context around them
sed -n '1400,1470p' devices/android.goRepository: mobile-next/mobilecli
Length of output: 1360
Propagate scanner and subprocess errors instead of silently returning success.
The StreamLogs function declares error as its return type but always returns nil, discarding both scanner.Err() and cmd.Wait() results. Device disconnects, adb failures, broken pipes, and scanner read errors are invisible to callers. The distinction between intentional stop (when onLog returns false) and actual errors must be preserved.
Suggested fix
scanner := bufio.NewScanner(stdout)
+ scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
+ stoppedByCaller := false
for scanner.Scan() {
line := scanner.Text()
@@
if !onLog(entry) {
+ stoppedByCaller = true
_ = cmd.Process.Kill()
break
}
}
- _ = cmd.Wait()
- return nil
+ if err := scanner.Err(); err != nil && !stoppedByCaller {
+ _ = cmd.Process.Kill()
+ _ = cmd.Wait()
+ return fmt.Errorf("failed reading logcat output: %w", err)
+ }
+
+ if err := cmd.Wait(); err != nil && !stoppedByCaller {
+ return fmt.Errorf("logcat exited unexpectedly: %w", err)
+ }
+ return nil
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| scanner := bufio.NewScanner(stdout) | |
| for scanner.Scan() { | |
| line := scanner.Text() | |
| parsed := parseLogcatLine(line) | |
| if parsed == nil { | |
| continue | |
| } | |
| pid, _ := strconv.Atoi(parsed.PID) | |
| level := logcatLevelMap[parsed.Level] | |
| if level == "" { | |
| level = parsed.Level | |
| } | |
| entry := LogEntry{ | |
| Timestamp: parsed.Date + " " + parsed.Time, | |
| PID: pid, | |
| Level: level, | |
| Tag: parsed.Tag, | |
| Message: parsed.Message, | |
| } | |
| // resolve process name from ps map (for --process filtering) | |
| if pidMap != nil { | |
| entry.Process = pidMap[pid] | |
| } | |
| if !onLog(entry) { | |
| _ = cmd.Process.Kill() | |
| break | |
| } | |
| } | |
| _ = cmd.Wait() | |
| return nil | |
| scanner := bufio.NewScanner(stdout) | |
| scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) | |
| stoppedByCaller := false | |
| for scanner.Scan() { | |
| line := scanner.Text() | |
| parsed := parseLogcatLine(line) | |
| if parsed == nil { | |
| continue | |
| } | |
| pid, _ := strconv.Atoi(parsed.PID) | |
| level := logcatLevelMap[parsed.Level] | |
| if level == "" { | |
| level = parsed.Level | |
| } | |
| entry := LogEntry{ | |
| Timestamp: parsed.Date + " " + parsed.Time, | |
| PID: pid, | |
| Level: level, | |
| Tag: parsed.Tag, | |
| Message: parsed.Message, | |
| } | |
| // resolve process name from ps map (for --process filtering) | |
| if pidMap != nil { | |
| entry.Process = pidMap[pid] | |
| } | |
| if !onLog(entry) { | |
| stoppedByCaller = true | |
| _ = cmd.Process.Kill() | |
| break | |
| } | |
| } | |
| if err := scanner.Err(); err != nil && !stoppedByCaller { | |
| _ = cmd.Process.Kill() | |
| _ = cmd.Wait() | |
| return fmt.Errorf("failed reading logcat output: %w", err) | |
| } | |
| if err := cmd.Wait(); err != nil && !stoppedByCaller { | |
| return fmt.Errorf("logcat exited unexpectedly: %w", err) | |
| } | |
| return nil |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@devices/android.go` around lines 1424 - 1459, StreamLogs currently swallows
scanner and subprocess errors; change it to propagate them: after the scanner
loop check scanner.Err() and return it if non-nil (unless we intentionally
stopped because onLog returned false), and after breaking/killing the child call
cmd.Wait() and return its error instead of always returning nil; use the onLog
return value to distinguish an intentional stop (treat as nil) from actual
errors, and ensure any error from cmd.Wait() is returned when the stop was not
intentional. Reference: StreamLogs function, scanner variable, onLog callback,
and cmd.Wait()/cmd.Process.Kill() calls.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
devices/simulator.go (1)
925-932: Optional: deduplicate process termination/wait error plumbing.Line 925, Line 930, and Line 943 repeat the same kill+wait pattern. A small local helper would reduce repetition and keep future error-path changes safer.
Refactor sketch
func (s *SimulatorDevice) StreamLogs(onLog func(LogEntry) bool) error { @@ - // read opening '[' of the JSON array + terminateWithDecodeErr := func(prefix string, cause error) error { + _ = cmd.Process.Kill() + waitErr := cmd.Wait() + if waitErr != nil { + return fmt.Errorf("%s: %w (process exit: %v)", prefix, cause, waitErr) + } + return fmt.Errorf("%s: %w", prefix, cause) + } + + // read opening '[' of the JSON array token, err := decoder.Token() if err != nil { - _ = cmd.Process.Kill() - waitErr := cmd.Wait() - return fmt.Errorf("failed to read opening token: %w (process exit: %v)", err, waitErr) + return terminateWithDecodeErr("failed to read opening token", err) } if delim, ok := token.(json.Delim); !ok || delim != '[' { - _ = cmd.Process.Kill() - waitErr := cmd.Wait() - return fmt.Errorf("expected '[', got %v (process exit: %v)", token, waitErr) + return terminateWithDecodeErr(fmt.Sprintf("expected '[', got %v", token), errors.New("invalid opening token")) } @@ if err := decoder.Decode(&raw); err != nil { if errors.Is(err, io.EOF) { break } - _ = cmd.Process.Kill() - waitErr := cmd.Wait() - return fmt.Errorf("failed to decode log entry: %w (process exit: %v)", err, waitErr) + return terminateWithDecodeErr("failed to decode log entry", err) }Also applies to: 943-945, 959-966
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@devices/simulator.go` around lines 925 - 932, The error paths repeatedly call cmd.Process.Kill() followed by cmd.Wait() and propagate wait errors (e.g., the blocks handling token errors around the token/json.Delim checks), so extract that plumbing into a small helper (for example killAndWait or terminateCmd) that accepts the *exec.Cmd and returns the wait error (or combined error), then replace each duplicated pair (the occurrences using cmd.Process.Kill() and cmd.Wait() in the token read/expect blocks) with a single call to that helper and use its returned error in the fmt.Errorf calls to keep semantics identical while removing duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@devices/ios.go`:
- Around line 1632-1673: The StreamLogs method calls the callback onLog without
checking for nil which can panic; add a nil guard around the invocation in
IOSDevice.StreamLogs so callers may pass nil safely. Specifically, in function
StreamLogs (method on type IOSDevice) replace the unconditional call to
onLog(out) with a check like: if onLog != nil { if !onLog(out) { return nil } }
so that when onLog is nil the log loop continues/terminates normally without
invoking a nil function pointer.
---
Nitpick comments:
In `@devices/simulator.go`:
- Around line 925-932: The error paths repeatedly call cmd.Process.Kill()
followed by cmd.Wait() and propagate wait errors (e.g., the blocks handling
token errors around the token/json.Delim checks), so extract that plumbing into
a small helper (for example killAndWait or terminateCmd) that accepts the
*exec.Cmd and returns the wait error (or combined error), then replace each
duplicated pair (the occurrences using cmd.Process.Kill() and cmd.Wait() in the
token read/expect blocks) with a single call to that helper and use its returned
error in the fmt.Errorf calls to keep semantics identical while removing
duplication.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 8d0fedcd-9eba-4938-b8a2-ac8010ee693d
📒 Files selected for processing (2)
devices/ios.godevices/simulator.go
| func (d *IOSDevice) StreamLogs(onLog func(LogEntry) bool) error { | ||
| // ensure tunnel is running for iOS 17+ | ||
| err := d.startTunnel() | ||
| if err != nil { | ||
| return fmt.Errorf("failed to start tunnel: %w", err) | ||
| } | ||
|
|
||
| device, err := d.getEnhancedDevice() | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get device: %w", err) | ||
| } | ||
|
|
||
| spec := ostrace.DefaultLevelFilter() | ||
| conn, err := ostrace.New(device, -1, spec.MessageFilter, spec.StreamFlags) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to connect to os_trace_relay: %w", err) | ||
| } | ||
| defer func() { _ = conn.Close() }() | ||
|
|
||
| for { | ||
| entry, err := conn.ReadEntry() | ||
| if err != nil { | ||
| if errors.Is(err, io.EOF) { | ||
| return nil | ||
| } | ||
| return fmt.Errorf("os_trace read error: %w", err) | ||
| } | ||
|
|
||
| out := LogEntry{ | ||
| Timestamp: entry.Timestamp.Format("2006-01-02 15:04:05.000000-0700"), | ||
| Message: entry.Message, | ||
| Level: entry.LevelName, | ||
| PID: int(entry.PID), | ||
| Process: processNameFromPath(entry.Filename), | ||
| } | ||
| if entry.Label != nil { | ||
| out.Subsystem = entry.Label.Subsystem | ||
| out.Category = entry.Label.Category | ||
| } | ||
|
|
||
| if !onLog(out) { | ||
| return nil |
There was a problem hiding this comment.
Add a nil guard for onLog to prevent panic.
At Line 1672, onLog is called unconditionally. If a caller passes nil, this will panic and terminate the command path.
Proposed fix
func (d *IOSDevice) StreamLogs(onLog func(LogEntry) bool) error {
+ if onLog == nil {
+ return fmt.Errorf("onLog callback cannot be nil")
+ }
+
// ensure tunnel is running for iOS 17+
err := d.startTunnel()
if err != nil {
return fmt.Errorf("failed to start tunnel: %w", err)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| func (d *IOSDevice) StreamLogs(onLog func(LogEntry) bool) error { | |
| // ensure tunnel is running for iOS 17+ | |
| err := d.startTunnel() | |
| if err != nil { | |
| return fmt.Errorf("failed to start tunnel: %w", err) | |
| } | |
| device, err := d.getEnhancedDevice() | |
| if err != nil { | |
| return fmt.Errorf("failed to get device: %w", err) | |
| } | |
| spec := ostrace.DefaultLevelFilter() | |
| conn, err := ostrace.New(device, -1, spec.MessageFilter, spec.StreamFlags) | |
| if err != nil { | |
| return fmt.Errorf("failed to connect to os_trace_relay: %w", err) | |
| } | |
| defer func() { _ = conn.Close() }() | |
| for { | |
| entry, err := conn.ReadEntry() | |
| if err != nil { | |
| if errors.Is(err, io.EOF) { | |
| return nil | |
| } | |
| return fmt.Errorf("os_trace read error: %w", err) | |
| } | |
| out := LogEntry{ | |
| Timestamp: entry.Timestamp.Format("2006-01-02 15:04:05.000000-0700"), | |
| Message: entry.Message, | |
| Level: entry.LevelName, | |
| PID: int(entry.PID), | |
| Process: processNameFromPath(entry.Filename), | |
| } | |
| if entry.Label != nil { | |
| out.Subsystem = entry.Label.Subsystem | |
| out.Category = entry.Label.Category | |
| } | |
| if !onLog(out) { | |
| return nil | |
| func (d *IOSDevice) StreamLogs(onLog func(LogEntry) bool) error { | |
| if onLog == nil { | |
| return fmt.Errorf("onLog callback cannot be nil") | |
| } | |
| // ensure tunnel is running for iOS 17+ | |
| err := d.startTunnel() | |
| if err != nil { | |
| return fmt.Errorf("failed to start tunnel: %w", err) | |
| } | |
| device, err := d.getEnhancedDevice() | |
| if err != nil { | |
| return fmt.Errorf("failed to get device: %w", err) | |
| } | |
| spec := ostrace.DefaultLevelFilter() | |
| conn, err := ostrace.New(device, -1, spec.MessageFilter, spec.StreamFlags) | |
| if err != nil { | |
| return fmt.Errorf("failed to connect to os_trace_relay: %w", err) | |
| } | |
| defer func() { _ = conn.Close() }() | |
| for { | |
| entry, err := conn.ReadEntry() | |
| if err != nil { | |
| if errors.Is(err, io.EOF) { | |
| return nil | |
| } | |
| return fmt.Errorf("os_trace read error: %w", err) | |
| } | |
| out := LogEntry{ | |
| Timestamp: entry.Timestamp.Format("2006-01-02 15:04:05.000000-0700"), | |
| Message: entry.Message, | |
| Level: entry.LevelName, | |
| PID: int(entry.PID), | |
| Process: processNameFromPath(entry.Filename), | |
| } | |
| if entry.Label != nil { | |
| out.Subsystem = entry.Label.Subsystem | |
| out.Category = entry.Label.Category | |
| } | |
| if !onLog(out) { | |
| return nil | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@devices/ios.go` around lines 1632 - 1673, The StreamLogs method calls the
callback onLog without checking for nil which can panic; add a nil guard around
the invocation in IOSDevice.StreamLogs so callers may pass nil safely.
Specifically, in function StreamLogs (method on type IOSDevice) replace the
unconditional call to onLog(out) with a check like: if onLog != nil { if
!onLog(out) { return nil } } so that when onLog is nil the log loop
continues/terminates normally without invoking a nil function pointer.
…onnections - thread context.Context through ControllableDevice.StreamLogs interface - use exec.CommandContext on Android and Simulator so the subprocess is killed automatically when the context is cancelled - replace blocking iOS ReadEntry loop with goroutine+channel+select to support ctx.Done without hanging on dead connections - wire signal.NotifyContext in CLI so Ctrl+C cancels the context cleanly - fix Android scanner: larger 256KB buffer and check scanner.Err() after loop
…roid resolve process names on demand via /proc/<pid>/cmdline instead of a one-shot ps snapshot at startup, so long-running streams stay accurate as processes come and go
No description provided.